@AspectJ 可以将切面声明为普通的 Java 类。 AspectJ 5发布的 AspectJ project 中引入了这种 @AspectJ 风格。Spring 使用了和 AspectJ 5 一样的注解,使用了 AspectJ 提供的一个库来做 pointcut(切点)解析和匹配。 但是,AOP 在运行时仍旧是纯的 Spring AOP,并不依赖于 AspectJ 的编译器或者 weaver(织入器)。
使用 AspectJ 的编译器或者 weaver(织入器)的话就可以使用完整的AspectJ 语言,我们将在9.8. Using AspectJ with Spring applications中讨论这个问题
为了在 Spring 配置中使用 @AspectJ aspects,你必须首先启用Spring 对基于 @AspectJ aspects 的配置支持,autoproxying(自动代理)基于通知是否来自这些切面。 自动代理是指 Spring 会判断一个bean 是否使用了一个或多个切面通知,并据此自动生成相应的代理以拦截其方法调用,并且确认通知是否如期进行。
通过 XML 或者 Java 配置来 启用 @AspectJ 支持。在任何情况下,你还需要确保 AspectJ aspectjweaver.jar
在你的应用程序的类路径中(版本 1.6.8 或以后)。这个库在 AspectJ 发布的 lib
目录中或通过Maven 的 中央存库得到。
使用 @Configuration 和 @EnableAspectJAutoProxy 注解:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
使用 aop:aspectj-autoproxy 元素:
<aop:aspectj-autoproxy/>
在启用 @AspectJ 支持的情况下,在application context中定义的任意带有一个 @Aspect 切面(拥有 @Aspect 注解)的 bean 都将被Spring自动识别并用于配置在 Spring AOP。 以下例子展示了为了完成一个不是非常有用的切面所需要的最小定义:
下面是在application context 中的一个常见的 bean 定义,这个 bean 指向一个使用了 @Aspect 注解的 bean 类:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of aspect here as normal -->
</bean>
下面是 NotVeryUsefulAspect 类定义,使用了 org.aspectj.lang.annotation.Aspect 注解。
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
切面(用 @Aspect 注解的类)和其他类一样有方法和字段定义。他们也可能包括切入点,通知和引入(inter-type)声明。
你可以注册 切面 像其他 bean 一样在 Spring 的 XML 进行配置,或自动通过类路径扫描-就像任何其他的 Spring 管理的 bean。然而,请注意,@Aspect 的注解是不足以在 classpath 自动检测到了:为了这个目的,你需要添加一个单独的 @Component 的注解(或者自定义stereotype annotation 即 qualifies,作为 Spring 组件的扫描规则)。
在 Spring AOP 中,它是不可能使自己的切面成为 其他切面的通知的目标的。类上的 @Aspect 注解标记 它成为一个切面,因此从 auto-proxying 中排除了它。
回想一下,切入点决定了连接点关注的内容,使得我们可以控制通知什么执行。 Spring AOP 只支持 Spring bean 方法执行连接点。所以你可以把切入点看做是匹配 Spring bean 上的方法执行。 一个切入点声明有两个部分:一个包含名字和任意参数的签名,还有一个切入点表达式,该表达式决定了我们关注那个方法的执行。 在 @AspectJ 中,一个切入点实际就是一个普通的方法定义提供的一个签名,并且切入点表达式使用 @Pointcut 注解来表示(这个方法的返回类型必须是 void)。
如下的例子定义了一个切入点'anyOldTransfer',这个切入点匹配了任意名为transfer
的方法执行
@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature
切入点表达式,也就是 @Pointcut 注解的值,是正规的 AspectJ 5 切入点表达式。 如果你想要更多了解 AspectJ 的 切入点语言,请参见AspectJ 编程指南(如果要了解扩展请参阅 AspectJ 5 开发手册) 或者其他人写的关于 AspectJ 的书,例如 Colyer et. al.著的《Eclipse AspectJ》或者 Ramnivas Laddad著的《AspectJ in Action》。
Spring AOP 支持在切入点表达式中使用如下的 AspectJ pointcut designators (切入点指定者 PCD) :
其他的切入点类型
完整的 AspectJ 切入点语言支持额外的切入点指定者,但是 Spring 不支持这个功能。 他们分别是call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this, 和 @withincode 。 在 Spring AOP 中使用这些指定者将会导致抛出IllegalArgumentException 异常。
在 Spring AOP 设置切入点指定者可能会在未来的发布中进行扩展,用来支持更多 AspectJ 切入点指定者。
因为 Spring AOP 限制了连接点必须是方法执行级别的,pointcut designators 的讨论也给出了一个定义,这个定义和 AspectJ 的编程指南中的定义相比显得更加狭窄。 除此之外,AspectJ 它本身有基于类型的语义,在执行的连接点'this'和'target'都是指同一个对象,也就是执行方法的对象。 Spring AOP 是一个基于代理的系统,并且严格区分代理对象本身(对应于'this')和背后的目标对象(对应于'target')
由于 Spring 的 AOP 框架基于代理的本质,protected 方法通过定义不拦截来实现的,无论是对 JDK 代理(这并不适用)还是 CGLIB 代理(这在技术上是可行的,但不值得推荐的用于AOP)。因此,任何给定的切入点将只匹配 public 方法!
如果您的拦截需求包括 protected/private 方法或者构造函数,考虑使用 Spring-driven 原生的 AspectJ 织入而不是 Spring 的基于代理的 AOP 框架。这就构成了一个 AOP 使用具有不同特点的不同模式,所以一定要在做决定之前首先要让自己熟悉织入。
Spring AOP 还支持另一个 PCD 命名的 bean
。PCD 允许您限制连接点匹配的特定命名的 Spring bean,或者一组名为 Spring bean(当使用通配符)。bean
PCD 有以下形式:
bean(idOrNameOfBean)
idOrNameOfBean 令牌是可以任何 Spring bean 的名称:使用 * 通配符支持字符提供限制,所以如果你为 Spring bean 建立一些命名约定可以很容易地编写一个bean
PCD表达出来。一样与其他切入点指示器、bean
PCD 可以是 &&'ed, ||'ed, 和 ! (否定)
注意:bean
PCD 只在 Spring AOP 支持-而不在原生的 AspectJ 织入。这个是 Spring 特有对 AspectJ 定义的 PCD 的扩展
切入点表达式可以使用 &&, || 和 ! 来合并.还可以通过名字来指向切入点表达式。 以下的例子展示了三种切入点表达式:anyPublicOperation
(在一个方法执行连接点代表了任意 public 方法的执行时匹配); inTrading
(在一个代表了在交易模块中的任意的方法执行时匹配) 和 tradingOperation
(在一个代表了在交易模块中的任意的公共方法执行时匹配)。
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
就上所示的,从更小的命名组件来构建更加复杂的切入点表达式是一种最佳实践。 当用名字来指定切入点时使用的是常见的Java成员可视性访问规则。 (比如说,你可以在同一类型中访问私有的切入点,在继承关系中访问受保护的切入点,可以在任意地方访问公共切入点。 成员可视性访问规则不影响到切入点的 匹配。
当开发企业级应用的时候,你通常会想要从几个切面来参考模块化的应用和特定操作的集合。 我们推荐定义一个“SystemArchitecture”切面来捕捉常见的切入点表达式。一个典型的切面可能看起来像下面这样:
package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.someapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.someapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.service..*)")
public void inServiceLayer() {}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.someapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.dao..*)")
public void inDataAccessLayer() {}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
* the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
public void businessService() {}
/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
定义的切入点可以指任何一个切面,你需要一个切入点表达式。例如,服务层的事务,你可以写:
<aop:config>
<aop:advisor
pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
在 9.3. Schema-based AOP support中讨论 <aop:config>
和 <aop:advisor>
标签。 在 12. Transaction Management 中讨论事务标签。
Spring AOP 用户可能会经常使用 execution
pointcut designator。执行表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)
throws-pattern?)
除了返回类型模式,名字模式和参数模式以外,所有的部分都是可选的。 返回类型模式决定了方法的返回类型必须依次匹配一个连接点。 你会使用的最频繁的返回类型模式是 *
,它代表了匹配任意的返回类型。 一个全称限定的类型名将只会匹配返回给定类型的方法。名字模式匹配的是方法名。 你可以使用*
通配符作为所有或者部分命名模式。 参数模式稍微有点复杂:()
匹配了一个不接受任何参数的方法, 而 (..)
匹配了一个接受任意数量参数的方法(零或者更多)。 模式 (*)
匹配了一个接受一个任何类型的参数的方法。 模式 (*,String)
匹配了一个接受两个参数的方法,第一个可以是任意类型,第二个则必须是String
类型。 请参见 AspectJ编程指南的 Language Semantics 部分。
下面给出一些常见切入点表达式的例子。
任意 public 方法的执行:
execution(public * *(..))
任何一个以“set”开始的方法的执行:
execution(* set*(..))
AccountService 接口的任意方法的执行:
execution(* com.xyz.service.AccountService.*(..))
定义在service包里的任意方法的执行:
execution(* com.xyz.service.*.*(..))
定义在service包或者子包里的任意方法的执行:
execution(* com.xyz.service..*.*(..))
在service包里的任意连接点(在Spring AOP中只是方法执行) :
within(com.xyz.service.*)
在service包或者子包里的任意连接点(在Spring AOP中只是方法执行) :
within(com.xyz.service..*)
实现了 AccountService 接口的代理对象的任意连接点(在Spring AOP中只是方法执行) :
this(com.xyz.service.AccountService)
'this'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得代理对象可以在通知体内访问到的部分。
实现了 AccountService 接口的目标对象的任意连接点(在Spring AOP中只是方法执行) :
target(com.xyz.service.AccountService)
'target'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得目标对象可以在通知体内访问到的部分。
任何一个只接受一个参数,且在运行时传入的参数实现了 Serializable 接口的连接点 (在Spring AOP中只是方法执行)
args(java.io.Serializable)
'args'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得方法参数可以在通知体内访问到的部分。
请注意在例子中给出的切入点不同于 execution(* *(java.io.Serializable))
只有在动态运行时候传入参数是可序列化的(Serializable)才匹配,而 execution 在传入参数的签名声明的类型实现了 Serializable 接口时候匹配。
有一个 @Transactional 注解的目标对象中的任意连接点(在Spring AOP 中只是方法执行)
@target(org.springframework.transaction.annotation.Transactional)
'@target' 也可以在binding form中使用:请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。
任何一个目标对象声明的类型有一个 @Transactional 注解的连接点(在Spring AOP中只是方法执行)
@within(org.springframework.transaction.annotation.Transactional)
'@within'也可以在binding form中使用:- 请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。
任何一个执行的方法有一个 @Transactional annotation的连接点(在Spring AOP中只是方法执行)
@annotation(org.springframework.transaction.annotation.Transactional)
'@annotation' 也可以在binding form中使用:- 请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。
任何一个接受一个参数,并且传入的参数在运行时的类型实现了 @Classified annotation的连接点(在Spring AOP中只是方法执行)
@args(com.xyz.security.Classified)
'@args'也可以在binding form中使用:- 请常见以下讨论通知的章节中关于如何使得 annotation 对象可以在通知体内访问到的部分。
任何 Spring bean 命名为 tradeService
的切入点(在Spring AOP中只是方法执行):
bean(tradeService)
任何 Spring bean 命名符合正则表达式为 *Service
的切入点(在Spring AOP中只是方法执行):
bean(*Service)
在编译过程中,AspectJ 执行切入点来尝试和优化匹配性能。检查代码确认每个连接点是否都匹配(静态或者动态)给出的切入点,这个代价是昂贵的。(动态匹配意味着匹配不能完全在静态分析上确认,而是用一个在代码运行时的代码测试来了来确认是否真实匹配了)。当第一次遇到切入点声明,AspectJ 将会重写一个最佳形式用于匹配执行。啥意思?基本切入点被重写为 DNF (Disjunctive Normal Form) ,切入点组件被分类,这样,评估检查代价更低。意味着无需担心不同切入点 designators(编号)之间的性能,可以任意顺序声明切入点来使用它们。
然而,AspectJ 只能工作于告诉他的事,为了优化匹配性能,你要考虑他们尝试获取和搜索空间来匹配可能的定义。现存的 designators(编号)主要有三类: kinded, scoping 和 context:
9.2.4 声明 advice
Advice 与切入点表单式相关联,并且与切入点匹配时执行 before, after, 或 around 方法。切入点表达式可以是一个命名切入点的引用,也可以是就地声明切入点表达式。
Before advice 使用 @Before 注解来声明
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
如果是就地切入点表达式,上面例子可以重写为:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
当匹配一个方法执行正常返回时,After returning advice 执行。声明使用 @AfterReturning 注解
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
注意:可以声明多个 advice
有时需要访问 advice 返回的真实值,可以使用 @AfterReturning 绑定到返回值:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
returning
属性对应了 advice 方法的参数名称,当方法执行返回,返回值将会传递给对于的 advice 方法的参数值。returning
条款还限制匹配只有那些返回一个指定类型值的方法执行(在本例中Object
,它将匹配任何返回值)。
注意:当使用 after-returning advice,不可能返回一个完全不同的引用。
当匹配到方法执行抛出异常时,After throwing advice 执行。使用 @AfterThrowing 注解:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
通常, 你只希望抛出给定的异常时才会运行通知,通常你也希望可以在通知内访问到抛出的异常。使用throwing
属性来限制抛出的异常(相反,使用Throwable作为异常类型可以匹配全部)并且绑定抛出的异常到通知的参数。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
throwing
属性中的名字必须要对应通知方法的参数名字,当一个方法执行时抛出异常退出,这个异常会被作为对应的参数传到通知的方法中。throwing
语句同样限制了这些方法必须抛出指定的异常(这里是DataAccessException
)。
无论方法是如何退出的,After (finally)通知都会执行。它通过@After
注解声明。After通知必须准备好处理正常和异常的返回。通常被用来释放资源等。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
最后一种通知是around通知。Around通知在匹配的方法执行时“包围”了它。它可以在方法执行前后工作,并决定何时,如何,是否最终要执行这个方法。Around通知经常在你需要以一种线程安全的方式在方法执行前后共享状态(比如开启或关闭一个定时器)的时候使用。总是要使用能满足你需求的最轻量级的通知(比如如果简单的befor通知能实现,就不要用around通知)。
Around通知用@Around
注解声明。通知方法的第一个参数必须要是ProceedingJoinPoint
类型。方法体内,调用ProceedingJoinPoint
的proceed()
会导致底层方法执行。proceed
方法在调用可以传入一个Object[]
数组,数组中的值会在方法执行的时候作为参数。
当proceed方法带有Object[]时,表现的行为与AspectJ编译器编译的around通知的proceed行为稍有不同。用传统的AspectJ语言编写的around通知,传递给proceed的参数数量必须和传递给around通知的参数数量相同(而不是底层连接点的参数数量),并且传递给procced的值取代了这个位置上连接点的实体绑定的原始值(如果你现在还没搞懂的话,不必担心)。Spring采取的方式更简单,并且更符合它基于代理的执行语义。如果你正在编译为Spring写的@AspectJ切面,并使用AspectJ编译器和织入器处理参数的话,你需要了解这种差异。有一种方法可以编写一个100%兼容Spring AOP和AspectJ的切面,这将在接下来advice paramters节中讨论。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
around通知的返回值将会作为返回值被调用方法的人看到。比如说一个简单缓存切面,如果已经有了缓存值可以直接返回,如果不存在缓存,就调用proceed()。注意around通知体内部,可以调用一次,多次甚至根本不调用proceed,这些都是合法的。
Spring提供了全类型的通知,这意味着你可以在通知的签名中声明你需要的参数(就上上面returning和throwing的例子一样),而不是一直使用Object[]
数组。我们将会看到如何将参数和其他上下问的值提供给通知内部。首先让我们看一下怎么编写通用的通知。
任何通知方法可以在第一个参数位置声明一个org.aspectj.lang.JointPoint
类型的参数(注意,around advice必须要求声明第一个参数为ProceedingJointPoint
类型,是JointPoint
的子类。)JointPoint
接口提供许多的方法,比如getArgs()
(返回方法的参数),getThis()
(返回代理对象),getTarget()
(返回目标对象),getSignature()
(返回被通知的方法的描述)和toString()
(打印被通知的方法的有效描述)。更多细节请查阅文档。
我们已经见到了如何绑定返回值或是返回的异常(用after returnming和after throwing通知)。为了使得通知内部可以访问参数值,你可以使用args
的绑定形式。如果在参数表达式中使用参数名字代替参数类型,那么当通知执行的时候,对应的参数值会被传入。一个简答的例子让这变得清楚点。假设你希望在一个第一个参数为Account对象的DAO方法添加通知,并需要在通知体内访问account。你可以像下面这样写:
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
切点表带中args(account,..)
的部分有连个目的:第一,它限制了执行的方至少需要一个参数,且参数必须是Account
实例;第二,它让通知通过account
参数可以访问实际Account
对象。
另一只写法是声明一个符合连接点且可以“提供”Account
实例的切点,然后在其他通知上通过名字直接引用。像下面这样:、
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
感兴趣的读者可以查看AspectJ编码指导来获取更多信息。
代理对象(this
),目标对象对象(target
)和注解(@within
,@target
,@annotation
,@args
)可以用同样的方式绑定。下面的例子展示了你可以如何去匹配有@Auditable
注解的方法,并且提取出审核的编码。
首先,定义@Auditable
注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
然后定义执行匹配@Auditable
注解的方法的通知:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
Spring AOP在类定义和方法参数中接受泛型。假如你有下面这样的一个泛型类:
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
你可以通过简单指定通知的参数类型来限制拦截的具体方法的参数类型:
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
我们之前已经讨论过这明显是有用的,然而,必须要指出这对泛型容器是无效的,所以你不能像下面这样定义切点:
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}
为了让它起作用,我们必须拦截每种容器元素,这是不合理的,因为我们通常不知道怎么处理null
值。为了近似的实现这一点,你不得不将参数指为Collection<?>
,然后手动检查元素类型。
依赖于在切入点表达式中使用的匹配名称在通知调用时绑定到(通知和切入点)方法签名中的声明参数名称。参数名称不可以通过反射得到,所以Spring AOP使用下面的策略去决定参数名称:
* 如果参数的名称被使用者指明了,那么使用特指的名称:通知和切点注解都有一个可选的argNames
属性用来指出被注解的方法的名称-这些参数名称在运行时是可用的。比如:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code and bean
}
如果第一个参数类型是JointPoint
,ProceedingJointPoint
或者JointPoint.StaticPart
类型,你可以在argNames
中省略它的名字。比如,如果修改了通知接受了一个Joint Point对象,argNames
属性中不需要包含它:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code, bean, and jp
}
对于JoinPoint
,ProcceedingJointPoint
和JoinPoint
类型的第一个参数的特殊处理对不搜集其他连接点上下文的通知来说特别方便。这种情况下,你可以省略argNames
属性,比如,下面的例子就没有声明argNames
属性:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
*使用argNames
属性有点笨拙,因此在没有指定argNames
属性的时候,Spring AOP会查找类的调试信息从本地变量表中发现参数的名称。只要类编译的时候带有调试信息(至少有-g:vars
),这些信息就会存在。打开这个标志的编译结果是:(1)你的代码将会变得更加可读(逆向工程),(2)类文件会大一些(通常是无关紧要的),(3)删除无用本地变量的优化将不会被编译器应用。总之,打开这个标志编译时你不会遇到问题。
一个被AspectJ编译器编译过的@AspectJ切面,即使没有调试信息也没有添加argNames属性,但是编译任然会保留需要的信息。
如果代码在编译时没有添加所需的调试信息。那么Spring AOP将会尝试推断去配对绑定的变量和参数(比如,如果一个切点表达式只绑定了一个参数,并且通知的方法也只有一个参数,那这个配对就是很明显!)。如果绑定的参数给出的信息是模糊的,将会抛出AmbiguousBindingException
异常。
如果上面的策略都失败了,那么会抛出IllegalArgumentException
异常。
我们之前说到过怎么以Spring AOP和AspectJ都通用的方式去写一个带有参数的procceed。解决方法很简单,确保通知的签名按顺序绑定方法的参数。比如:
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
许多情况下,你都要像这样(上述的例子)绑定。
当许多通知想要运行在一个相同的连接点上时会发生什么?Spring AOP遵循和AspectJ相同的优先规则来决定通知执行的顺序。优先级高的通知会先“运行“进入”(因此如果有两个before通知,具有高高的优先级会先运行)。“退出”时,更高的优先级会后运行(所以给出两个after通知,优先级高的会在之后运行)。当定义在不同切面的两个通知要在相同的连接点运行时,除非你指定了,否则数序是未定义的。你可以通过指定优先级来控制执行顺序。这可以用常用的Spring做到,让切面类实现org.springframework.core.Ordered
接口或是用Order
注解注解它。对于给的两个切面,Ordered.getValue()
返回值(或是注解的值)低的,优先级更高。
当定义在一个切面中的两个通知要在相同的连接点上运行时,顺序是未定的(因为没有办法为javac编译过的类声明顺序)。可以考虑将这些通知方法合到一个通知里,或者是重构每个通知,放在不同的切面类里——然后以切面的层次进行排序。
引入(在AspectJ中被称为inter-type)使得切面能够声明被通知的对象实现给定的接口,且提供了这些对象对接口行为的实现。
引入可以用@DeclareParents
注解声明。这个注解被用来声明匹配的对象有一个新的父级(正如这个名字)。比如,给定一个接口UsageTracked
,和这个接口的实现DefaultUsageTracked
,下面的切面声明了所有实现了service的接口同样实现了UsageTracked
接口。(比如说为了通过JMX公开统计消息)
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
被实现的接口是由被注解的字段的类型决定的。@DeclareParents
的value
属性是一个AspectJ类型的样式:-任何符合的类型都会实现UsageTracked接口。注意上述例子before通知中,service beans可以直接被作为UsageTracked
接口的实现使用。当你用编码的方式访问一个bean,你可以像下面这么写:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
(这是一个高级的话题,如果你刚开始使用AOP,你可以安全的跳过它。)
默认的,在上下文内每一个切面都是单例。AspectJ把这叫做单例初始化模式。为切面定义其他的生命周期也是可以的:-Spring支持Aspectj的perthis
和pertarget
初始化模式(目前不支持percflow
,percflowbelow
和pertypewithin
目前还不支持)。
一个“perthis”切面可以通过@Aspect
注解中特指出perthis
来声明。让我们来看一个例子,然后我们会解释这是怎么工作的:
@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {
private int someState;
@Before(com.xyz.myapp.SystemArchitecture.businessService())
public void recordServiceUsage() {
// ...
}
}
perthis
的影响是当每个不同的service对象调用businessService时会创建一个切面实例(每一个符合切点表达式的不同对象在连接点被绑定到this)。切面实例会在service对象第一次调用这个方法是被创建。当service对象失效时,切面也会失效。在切面实例创建前,它内部的通知不会被执行。一旦切面实例被创建,它声明的通知将会在符合连接点地方执行,但是只有在关联service对象的那个切面的通知才会执行。更多关于per语句的信息请查看AspectJ的编码指导。
pertarget
初始化模式的工作方式和perthis相同,但是是为每一个对符合连接点的目标对象创建一个切面。
你已经看到了各个部分是如何工作的,现在让我们把它们整合起来做一些有用的事!
业务逻辑的服务有时候会因为并发的问题执行失败(比如,死锁)。如果一个操作再次尝试,那么很有可能在下一次成功。对于适合在这些情况下(不需要将冲突的解决返回给客户的操作)重试的业务逻辑服务,我们希望重试操作变透明避免客户段看在PessimisticLockingFailureException
。这需要清晰的在服务层横切多个服务,因此通过切面来实现是个好主意。
因为我们希望重试操作,我们需要使用around通知来多次调用procced。这是基础的切面实现:
@Aspect
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
注意切面实现了Orderd
接口,因此我们可以设置切面的优先级高于事物通知(我们希望每次尝试时都是新的事物)。maxRetries
和order
属性都可以通过SPring配置。around通知发生的最主要的动作在doConcurrentOperation
中。注意目前我们对全部的businessServices()s
都应用了重试逻辑。我们尝试proceed时,如果我们以一个PessimisticLockingFailureException
失败时,我们可以很容易的再次尝试,除非已经用完了重试的次数。
对应的Spring配置:
<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
为了改进切面,只重试idempotent操作,我们需要定义一个Idempotent
注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
并且使用这个注解去标注service操作的实现类。让切面只重试idempotent操作只需要简单改善切点表达式,让它至匹配@Idempotent
注解:
@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
...
}